
import fs from 'fs';
import path from 'path';
import { AIConfig } from "../AIConfig";
import { AICore } from "../AICore";
import { AIPanel } from "../AIPanel";
import { delay } from "../AIUtils";
import { IDEAgent } from "../IDEAgent";
import { post, uploadFile } from "../Imagen";
import { Thinkingdata } from "../Thinkingdata";
import { AITask } from "./AITask";

class GenAudioData{
    charactor=''
    text='生成语音示例,目前只支持中文，点击管理可以扩充音色。'
}

class TTSInfo{
    name:string;    //对应目录名
    audio:string;   //声音文件
    text:string;    //声音文件的文本
    id:string;      //服务器分配的id
}

interface ITTSConfigItem{
    id?:number; 
    dirname?:string; 
    isplaying?:boolean;
    name:string;
    url:string;
    audio?:HTMLAudioElement
    text?:string;
}

/** 音乐生成是一个单步任务 */

export class AITaskTTS extends AITask{
    data = IEditor.DataWatcher.watch(new GenAudioData());
    static EditObjName='TTS'
    resultAssetName:string=null;
    isManagePrompt=false;
    demoPreset:string[][]=[['男','4a17d48a1b3d240d590654b356f06f27a094eb9e'],['女','1c0651a9fa2fd9fffa2597221bff7afb98bcca7e']];
    presets:string[][]=[];

    constructor(){
        super();
        //读取配置文件
        //this.readConfig();
    }

    private preMakeDir(){
        let aipath = AICore.inst.AIPluginPath;
        if(!fs.existsSync(aipath +"/aiconfig/")){
            fs.mkdirSync(aipath +"/aiconfig/");
        }
        if (!fs.existsSync(aipath + '/aiconfig/ttsconfig/')) {
            fs.mkdirSync(aipath + '/aiconfig/ttsconfig/');
        }
    }

    private async syncConfigToServer(){
        let wmsg = await AIPanel.ins.startWaitMsg('更新预设音色');
        this.preMakeDir();
        await this.update_preset();
        AIPanel.ins.removeMsg(wmsg);
    }

    private async getItemString(item: DataTransferItem): Promise<string> {
        return new Promise<string>((res) => {
            item.getAsString(s => {
                res(s);
            });
        })
    }    

    private async getDropAudioFile(event:any){
        let item = event.items[0];
        if (item.kind == 'string') {
            let s = await this.getItemString(item);
            if (s.startsWith(':')) {
                //这个是目录？
            }
            else {
                let fp: string;
                //文件
                if (s.startsWith('file:///')) {
                    fp = s.substring('file:///'.length);
                } else {
                    //具体的资源uuid
                    //await AICore.inst.memory.addFile(s, 'user');
                    //展示出来
                    let asset = await Editor.assetDb.getAsset(s);
                    fp = Editor.assetDb.getFullPath(asset);
                }
                //temp
                let ext = path.extname(fp).toLowerCase();
                if (ext == '.mp3' || ext == '.wav') {
                    return fp;
                } 
            }
        } else if (item.kind == 'file') {
            let f = item.getAsFile();
            if(!f) return null;
            //await AICore.inst.memory.addFile(f, 'user');
            let type = f.type;
            if (type.startsWith('audio')) {
                return (f as any).path;
            }
        }
        return null;
    }

    private async calcSha1(buffer:Uint8Array){
        // 获取SubtleCrypto上下文  
        const crypto = window.crypto.subtle;  
        
        // 计算MD5哈希值  
        const hash = await crypto.digest('SHA-1', buffer);  
        let buffhash = Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
        return buffhash;
    }

    private traverseDirectory(directory: string) {
        fs.readdir(directory, (err, files) => {
            if (err) {
                console.error('Error reading directory:', err);
                return;
            }

            files.forEach((file) => {
                const fullPath = path.join(directory, file);
                const stats = fs.statSync(fullPath);

                if (stats.isDirectory()) {
                    //console.log('Directory:', file);
                    this.traverseDirectory(fullPath); // 递归遍历子目录  
                } else {
                    //console.log('File:', file);
                }
            });
        });
    }

    private async createAudio(file:string){
        return new Promise<HTMLAudioElement>(res=>{
            var audio = new Audio(file);
            audio.addEventListener('loadedmetadata', function(){
                res(audio);
            })
        });
    }

    /**
     * 更新一个目录。如果需要更新就返回true，并且参数的hashinfo保存hash
     * @param dir 
     * @param hashinfo 
     * @returns 
     */
    private async updatePresetDir(dir:string,hashinfo:{wavehash:string,texthash:string}){
        let files = fs.readdirSync(dir);
        if(!files)return false;

        let wavhash:string=null;
        let txthash:string=null;
        let infos:string[]=[];
        let ret:{wave:File,text:File}={wave:null, text:null}
        for(let i=0,n=files.length; i<n; i++){
            let file = files[i];
            const fullPath = path.join(dir, file);
            const stats = fs.statSync(fullPath);
            if (!stats.isDirectory()) {
                //找到wav，找到txt，找到info
                //if(file.endsWith('.wav')){
                if(file=='sound.wav'){
                    let audio = await this.createAudio(fullPath);
                    //console.log(file, 'audio duration:', audio.duration)
                    if(audio.duration>20){
                        alert('Error:'+fullPath+' \n这个文件太长，要求声音在10秒左右');
                        return false;
                    }
                    if(audio.duration<2){
                        alert('Error:'+fullPath+' \n这个文件太短，要求声音在10秒左右');
                        return false;
                    }
                    let buff = fs.readFileSync(fullPath);
                    let fileobj = new File([ new Uint8Array(buff.buffer,buff.byteOffset,buff.length)],file,{type: "application/octet-stream"})
                    ret.wave=fileobj;
                    if(buff&&buff.buffer){
                        wavhash = await this.calcSha1(new Uint8Array(buff.buffer,buff.byteOffset,buff.length))
                    }
                }
                if(file.toLowerCase()=='info.txt'){
                    let cc = fs.readFileSync(fullPath,{encoding:'utf-8'});
                    infos = cc.split(' ');
                //}else if(file.endsWith('.txt')){
                }else if(file=='text.txt'){
                    let buff = fs.readFileSync(fullPath);
                    if(buff&&buff.buffer){
                        txthash = await this.calcSha1(new Uint8Array(buff.buffer,buff.byteOffset,buff.length))
                    }
                    let fileobj = new File([new Uint8Array(buff.buffer,buff.byteOffset,buff.length)],file,{type: "application/octet-stream"})   //先用二进制 不行就用 {type: 'text/plain'}
                    ret.text=fileobj;
                }
            }
        }

        if(wavhash && txthash && (wavhash!=infos[0]||txthash!=infos[1])){
            //必须要wav文件和txt文件都有才行
            //需要更新
            let aa = await uploadFile(AIConfig.TTS_Vallex_PromptUploadUrl,ret)
            fs.writeFileSync(path.join(dir, 'info.txt'),`${aa.wavehash} ${aa.texthash}`)
            hashinfo.wavehash=aa.wavehash;
            hashinfo.texthash=aa.texthash;
            Thinkingdata.ins.track("update_tts_preset",{name:dir});
            return true;
        }
        hashinfo.wavehash=wavhash;
        hashinfo.texthash=txthash;
        return true;
    }

    findIDByName(name:string){
        for(let i=0,n=this.presets.length; i<n; i++){
            if(name==this.presets[i][0])
                return this.presets[i][1];
        }
        for(let i=0,n=this.demoPreset.length; i<n; i++){
            if(name==this.demoPreset[i][0])
                return this.demoPreset[i][1];
        }
        return null;
    }

    private hasID(id:string){
        for(let i=0,n=this.presets.length; i<n; i++){
            if(id==this.presets[i][1])
                return true;
        }
        for(let i=0,n=this.demoPreset.length; i<n; i++){
            if(id==this.demoPreset[i][1])
                return true;
        }
        return false;
    }
    async update_preset(){
        let dir = AICore.inst.AIPluginPath + '/aiconfig/ttsconfig/';
        let files = fs.readdirSync(dir);
        if(!files) return;
        let all_preset:string[][]=[];
        for(let i=0,n=files.length; i<n; i++){
            let file = files[i];
            const fullPath = path.join(dir, file);
            const stats = fs.statSync(fullPath);
            if (stats.isDirectory()) {
                //console.log('Directory:', file);
                let hashinfo:{wavehash:string,texthash:string} = {wavehash:null,texthash:null};
                if(await this.updatePresetDir(fullPath,hashinfo)){
                    all_preset.push([file,hashinfo.wavehash]);
                }
            }
        }
        this.presets=all_preset;

        if(this.hasID(this.data.charactor)){
        }else{//如果当前的id不在预设中，则设置为第一个
            this.data.charactor=this.demoPreset[0][0];
        }
    }

    async applyConfigData(datas:ITTSConfigItem[]){
        /**
         * 测试：
         *  wav就在同一个目录
         *  写入文本与原来的hash要一致
         *  改名的问题（先拷贝再改名）
         */
        let cfg_dir = AICore.inst.AIPluginPath + '/aiconfig/ttsconfig/';
        //先统计所有的，用来做删除
        let alldir:string[]=[];
        let files = fs.readdirSync(cfg_dir);
        if(files){
            for(let i=0,n=files.length; i<n; i++){
                let file = files[i];
                const fullPath = path.join(cfg_dir, file);
                const stats = fs.statSync(fullPath);
                if (stats.isDirectory()) alldir.push(file);
            }
        }

        //先拷贝文件，防止后面改目录名之后找不到文件了
        datas.forEach(data=>{
            if(!data.url) return;
            let dstPath = path.join(cfg_dir,data.dirname??data.name);  //优先dirname，如果需要改名，下面再改
            if(!data.dirname && !fs.existsSync(dstPath)){
                fs.mkdirSync(dstPath);
            }
            let srcpath = path.dirname(data.url);
            //如果已经在当前目录下
            if(path.relative(dstPath,srcpath)==''){
            }else{
                //拷贝
                let dstfile = 'sound.wav';// path.basename(data.url);
                fs.cpSync(data.url,path.join(dstPath,dstfile));
            }
            //文本文件
            fs.writeFileSync(path.join(dstPath,'text.txt'),data.text);
        });

        //然后改目录名
        datas.forEach(data=>{
            //如果是改名
            if(data.dirname && data.dirname!=data.name){
                try{
                fs.renameSync(path.join(cfg_dir,data.dirname), path.join(cfg_dir,data.name));
                }catch(e){
                    alert(`重命名[${path.join(cfg_dir,data.dirname)}->${path.join(cfg_dir,data.name)}]失败，
检查一下是不是原目录被别的应用打开了。都关闭后再重试。`);
                }
            }
        });
        // 删除
        datas.forEach(data=>{
            let idx = alldir.indexOf(data.name);
            if(idx<0)return;
            alldir.splice(idx,1);
        });
        alldir.forEach(dir=>{
            fs.rmSync(path.join(cfg_dir,dir),{force:true,recursive:true});
        });

        //同步到服务器,更新 this.presets
        await this.syncConfigToServer();
    }

    private async readPresetDir(dir:string,presetInfo:{wavefile:string,text:string}){
        let files = fs.readdirSync(dir);
        if(!files)return false;

        for(let i=0,n=files.length; i<n; i++){
            let file = files[i];
            const fullPath = path.join(dir, file);
            const stats = fs.statSync(fullPath);
            if (!stats.isDirectory()) {
                if(file.endsWith('.wav')){
                    presetInfo.wavefile = fullPath;
                }
                if(file.toLowerCase()=='info.txt'){
                }else if(file.endsWith('.txt')){    //要求只能有一个文件，否则这个会与wav对应不上
                    presetInfo.text = fs.readFileSync(fullPath,{encoding:'utf-8'});
                }
            }
        }
        return true;
    }    

    async readConfig():Promise<ITTSConfigItem[]>{
        let configs:ITTSConfigItem[] = []
        //AICore.inst.startWaitMsg('读取预设音色');
        await delay(1); //给个显示机会
        this.preMakeDir();
        let dir = AICore.inst.AIPluginPath + '/aiconfig/ttsconfig/';
        let files = fs.readdirSync(dir);
        if(!files){
            //AICore.inst.endWaitMsg();
            return null;
        }

        for(let i=0,n=files.length; i<n; i++){
            let file = files[i];
            const fullPath = path.join(dir, file);
            const stats = fs.statSync(fullPath);
            if (stats.isDirectory()) {
                //console.log('Directory:', file);
                let cpreset:{wavefile:string,text:string}={wavefile:null,text:null}
                if(this.readPresetDir(fullPath,cpreset)){
                    configs.push({name:file,dirname:file,url:cpreset.wavefile, text:cpreset.text});
                }
            }
        }
        //AICore.inst.endWaitMsg();
        return configs;
    }

    async showConfigUI(parentui:gui.Widget,msgid:string){
        await gui.UIPackage.resourceMgr.load('editorResources/AIUI/AIGenTTSItem.widget')
        let cont = await gui.UIPackage.createWidget("editorResources/AIUI/AIGenTTSConfig.widget");
        cont.name='widget';
        let ok = cont.getChild('ok') as gui.Button;
        let cancel = cont.getChild('cancel') as gui.Button;
        let add = cont.getChild('addvoice') as gui.Button;
        let list = cont.getChild("voices") as gui.List;
        let close = cont.getChild('WindowCloseButton') as gui.Button

        close.on('click',()=>{
            if(parentui){
                AIPanel.ins.updateMsg(parentui,msgid);
            }
        });

        //TODO 收集现在的信息
        //dirname是原始目录名称，如果与name不一致了，则是修改了
        let configdata = await this.readConfig();
        function del_config_item_data(data:ITTSConfigItem){
            let idx = configdata.indexOf(data);
            if(idx>=0){
                configdata.splice(idx,1);
            }
        }

        function addconfigItem(itemdata:ITTSConfigItem){
            // let itemdata: ITTSConfigItem= 
            //     {isplaying:false,name:null, dirname:null, url:null, audio:null};
            let l = gui.UIPackage.createWidgetSync("editorResources/AIUI/AIGenTTSItem.widget");
            let txt = l.getChild('text');
            let drop = l.getChild('drop');
            let del = l.getChild('delete');
            let name_= l.getChild('name');
            let play = l.getChild('play');
            let dragtip = l.getChild('dragtip');

            list.addChild(l);
            l.width = list.width;
            l.addRelation(list, gui.RelationType.Width);  
            txt&&(txt.text = itemdata.text);
            name_.text=itemdata.name;
            //红框，用来指示文件拖到自己身上了

            if(itemdata.url){
                let audio = new Audio(itemdata.url);
                itemdata.audio = audio;
                drop.visible=false;
            }

            function dragover(evt: gui.Event){
                var dt: DataTransfer = evt.data;
                dt.dropEffect = "copy";
                evt.preventDefault();
                dragtip.visible=true;
            }
            function dragleave(evt:gui.Event){
                dragtip.visible=false;
                evt.preventDefault();
            }
            async function dropfunc(evt:gui.Event){
                var dt: DataTransfer = evt.data;
                dt.dropEffect = "copy";
                evt.preventDefault();
                dragtip.visible=false;
    
                let r = await me.getDropAudioFile(evt.data) as string;
                if(!r || !r.endsWith('.wav')){
                    alert('只支持wav文件');
                    return;
                }
                let audio = new Audio(r);
                itemdata.audio = audio;
                itemdata.url=r;
                drop.visible=false;
            }
            drop.on("native_dragover", dragover, null);
            drop.on("native_drop", dropfunc, null);
            drop.on('native_dragleave',dragleave,null);
            txt.on("native_dragover", dragover, null);
            txt.on("native_drop", dropfunc, null);
            txt.on('native_dragleave',dragleave,null);

            txt.on('submit',(e:gui.Event)=>{
                itemdata.text=(e.target as gui.TextInput).text;
            },null);
            name_.on('submit',(e:gui.Event)=>{
                let namev = (e.target as gui.TextInput).text;
                namev = getAName(namev)
                itemdata.name=namev;
                name_.text=namev;
            },null);

            play.on('click',()=>{
                let data = itemdata;
                let audio = data.audio;
                if(!audio) 
                    return;
                if(data.isplaying){
                    data.isplaying=false;
                    play.text='＞'
                    audio.pause();
                }else{
                    data.isplaying=true;
                    audio.play();
                    play.text='||'
                    audio.addEventListener("ended", function() {
                        data.isplaying=false;
                        play.text='＞'
                    });                                        
                }
            },null);
            del.on('click',()=>{
                list.removeChild(l);
                //删除数据
                del_config_item_data(itemdata);
            },null);
        }

        //添加已有的配置
        for(let i=0,n=configdata.length; i<n; i++){
            let cpreset = configdata[i];
            addconfigItem(cpreset);
        }

        ok.on('click',async (e:gui.Event)=>{
            //应用语音的设置。关闭界面
            //根据数据修改配置目录
            //同步到服务器
            await this.applyConfigData(configdata);
            if(parentui){
                this.ui_update_Preset(parentui);
                AIPanel.ins.updateMsg(parentui,msgid);
            }
        },this);
        cancel.on('click',(e:gui.Event)=>{
            //放弃设置，关闭界面
            if(parentui){
                AIPanel.ins.updateMsg(parentui,msgid);
            }
        },this);

        let me = this;
        //得到一个不重名的名字。如果temp没有重名的直接返回，否则就在后面加数字
        function getAName(temp:string){
            let n = 0; //第一个是temp不加数字
            for(let tn=0; tn<100; tn++){
                let test = n==0?temp:(temp+n);
                let find=false;
                for(let i=0,n=configdata.length; i<n; i++){
                    if(configdata[i].name==test){
                        find=true;
                        break;
                    }
                }
                if(!find) return test;
                n++;
            }
            return String(Date.now());
        }
        add.on('click', (e:gui.Event)=>{
            let item:ITTSConfigItem={name:getAName('名字'),text:'',url:null};
            configdata.push(item);
            addconfigItem(item);
        },this);

        list.on('size_changed', () => {
            cont.height = ok.height + ok.y + 10;
        });
        return cont;
    }

    private ui_update_Preset(ui:gui.Widget, voicelist?:gui.ComboBox){
        if(!voicelist){
            voicelist= ui.getChild('voices') as gui.ComboBox;
        }
        let values:string[]=[];
        this.demoPreset.forEach(p=>{
            values.push(p[0]);
        })
        this.presets.forEach(p=>{
            values.push(p[0]);  //0 name 1 hash
        })
        voicelist.values = values;
        voicelist.items = values;
        let idx = values.indexOf(this.data.charactor);
        voicelist.selectedIndex = idx>=0?idx:0; //设置选择的音色
    }

    async showUI(){
        //return await this.showConfigUI();

        //await gui.UIPackage.resourceMgr.load("editorResources/AIUI/AIGenTTS.widget");
        await gui.UIPackage.resourceMgr.load('editorResources/AIUI/AIGenTTSConfig.widget');
        let cont = await gui.UIPackage.createWidget("editorResources/AIUI/AIGenTTS.widget");
        cont.name='widget'; //aipanel根据这个来查找，并设置宽度

        let text = cont.getChild('value') as gui.TextField;
        //音色设置
        let voice = cont.getChild('voices') as gui.ComboBox;
        let gen = cont.getChild('generate') as gui.Button;
        let close = cont.getChild('WindowCloseButton') as gui.Button;
        let manage = cont.getChild('managevoice') as gui.Button;

        text.text = this.data.text;
        text.on('submit',(e:gui.Event)=>{
            this.data.text=(e.target as gui.TextField).text;
        },this);

        //let config = await this.readConfig();
        this.ui_update_Preset(cont,voice);
        voice.on('changed',(e:gui.Event)=>{
            let txt = (e.target as any).value;
            this.data.charactor=txt;
        },this);

        gen.on('click',(e:gui.Event)=>{
            this.textToSpeech(this.data.text,this.data.charactor);
            //TODO 显示资源
        },this)

        let msgid = await AIPanel.ins.addMsg(cont,-1,{data:this.data});
        close.on('click', (e:gui.Event)=>{
            AIPanel.ins.removeMsg(msgid);
        }, this);

        manage.on('click',async (e:gui.Event)=>{
            let configui = await this.showConfigUI(cont,msgid);
            AIPanel.ins.updateMsg(configui, msgid);
        },this);

        return cont;
    }

    async textToSpeech(text:string, username:string){
        let data = this.data;
        
        // let dict:any = {叶兰:3, 将军:4, 神子:5, 班尼特:6,艾尔海森:7}
        // let userid=dict[username]
        // if(!userid){
        //     userid='4fe842e8d585c0f44189e2f570b4e57cf29dc86f'
        // }
        // if(!userid)
        //     userid=4
        let userid:string|number = this.findIDByName(username);
        if(!userid){
            userid=4;
        }
        let url = AIConfig.TTS_ValleX_Url;
        //埋点： vallextts 开始
        Thinkingdata.ins.track("genTTSStart", {role:username,text});
        let st = Date.now();
        let wmsg = await AIPanel.ins.startWaitMsg('生成中');
        let buff = await post(url,{
            text,
            id:userid //服务器现在是从1开始
        },'arraybuffer') as ArrayBuffer;
        let dt = (Date.now()-st)/1000;
        AIPanel.ins.removeMsg(wmsg);
        Thinkingdata.ins.track("genTTSEnd", {duration:dt});
        if(!buff){
            return null;
        }

        //Thinkingdata.ins.track("GenTTS",{duration:101})
        //埋点： vallextts 结束

        let ideAgent= IDEAgent.inst;
        let file = await ideAgent.saveBuffToTmp(Buffer.from(buff), 'wav');
        let assetname = `tts_${username}_${text}_${Date.now()}.wav`;
        if(!assetname.endsWith('.wav')){
            assetname+='.wav';
        }

        let asset = await ideAgent.crateAIAssetFromFile(file, 'tts', assetname);
        this.resultAssetName = asset.fileName;
        // 预览添加到聊天界面
        //AIPanel.ins.addSound(file,true);

        let cont = await gui.UIPackage.createWidget("editorResources/AIUI/AIGenTTSResult.widget");
        cont.name='widget'; //aipanel根据这个来查找，并设置宽度
        let name_ = cont.getChild('name') as gui.TextInput;
        let text_ = cont.getChild('text') as gui.TextField;
        let play_ = cont.getChild('play') as gui.Button;
        let regen_ = cont.getChild('regen') as gui.Button;
        let toasset_ = cont.getChild('toasset') as gui.Shape;

        name_.text = username+":";
        text_.text = text;
        let audio = new Audio(file);
        audio.play();
        play_.text='||';
        audio.addEventListener("ended", function() {
            play_.text='＞'
        });                                        

        text_.on('click',async ()=>{
            //let assetobj = await Editor.assetDb.getAsset(asset.fileName);
            Editor.scene?.setSelection([asset], "asset");            
        });
        toasset_.on('click', async ()=>{
            Editor.scene?.setSelection([asset], "asset");            
        });
        play_.on('click',()=>{
            if(play_.text=='＞'){
                audio.play();
                play_.text='||';
            }else{
                audio.pause();
                play_.text='＞';
            }
        })
        regen_.on('click',()=>{this.textToSpeech(text,username);});
        AIPanel.ins.addMsg(cont,-1);

        //AIPanel.ins.addMsg(`生成资源:${asset.fileName}<br><a onclick="$msgbindobj.onRegen(event)">重新生成</a>`,-1,{bindobj:{
        //}});//`<a href="javascript:AI.callTaskFunc(${this.taskid},'onClickResult',null)">${this.mtlName}</a>`);        
        //
        return asset;
    }

    /**
     * 任务创建后会立即执行
     * 所以这里会包含更新界面的流程
     */
    async execute(){
        let data = this.data;
        //if(this.findID(data.charactor))
        await this.syncConfigToServer()
        if(!this.findIDByName(data.charactor)){
            //AIPanel.ins.addMsg('没有预设音色:'+data.charactor+',下面使用默认音色。',-1);
            if(this.presets.length>0)
                data.charactor=this.presets[0][0]
            else
                data.charactor=this.demoPreset[0][0];
        }
        this.showUI();
        //如果cancel的话，就返回最后的结果？

        return this.genAsset;
    }
}
